#region (c) 2014 THN Solutions LLC. - All rights reserved
/*
THN WEB SERVER

Copyright (c) 2014, THN Solutions LLC. ( www.ThnSolutions.com )
Author: Nguyễn, M. Hải                 ( www.Minh-Hai.com     )
All rights reserved.

This library is dual-licensed.
  + For use in commercial software, please obtain a commercial license from THN Solutions LLC.    

  + For use in free software, this library is licensed under GPL version 3. A summary of 
	GPLv3 is listed below. You should also find a copy of GPLv3 as file License_GPLv3.txt 
	included with the source-code files.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License version 3 as published by
    the Free Software Foundation  of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#endregion

#region Record of Change
//             							CHANGE HISTORY
// -------------------------------------------------------------------------------------
// |   DATE    | DEVELOPER  | DESCRIPTION                                              |
// -------------------------------------------------------------------------------------
// | 14-Nov-12 | HaiNM      | First creation.                                          |
// -------------------------------------------------------------------------------------
//
#endregion

#region Record of Audit
//             							REVIEW HISTORY
// -------------------------------------------------------------------------------------
// |   DATE    | REVIEWER   | NOTES                                                    |
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
//
#endregion

#region Using directives
using System;
using System.IO;

using Thn;
using Thn.IO;
using Thn.Net;
using Thn.Net.Http;
using Thn.Tasks;
using Thn.Logging;
#endregion

namespace Thn.Web
{
    /// <summary>
    /// Base class for all request handlers that work with files. This class provide common mechanisms to work with files.
    /// </summary>
#if DEBUG || TESTING
    public
#else
    public
#endif
 class FileHandlerBase : IHttpRequestHandler
    {
        #region Fields
        protected bool Trace = false;
        ObjectPool<FileSendInfo> mPool;
        #endregion

        #region Properties

        #region Path Translator
        protected IPathTranslator mPathTranslator;
        /// <summary>
        /// Gets or sets reference to the URI to local path translator.
        /// </summary>
        public IPathTranslator PathTranslator
        {
            get { return mPathTranslator; }
            set { mPathTranslator = value; }
        }
        #endregion

        #region File Service
        protected IFileService mFileService;
        /// <summary>
        /// Gets or sets reference to the file handling service.
        /// </summary>
        public IFileService FileService
        {
            get { return mFileService; }
            set { mFileService = value; }
        }
        #endregion

        #region Mime Types
        protected IMimeTypes mMimeTypes;
        /// <summary>
        /// Gets or sets reference to the known MIME types.
        /// </summary>
        public IMimeTypes MimeTypes
        {
            get { return mMimeTypes; }
            set { mMimeTypes = value; }
        }
        #endregion

        #endregion

        #region Send Chunk
        void SendChunk(FileSendInfo info)
        {
            //read chunk
            BodyOnlyHttpResponse response = new BodyOnlyHttpResponse();
            byte[] buffer = info.Buffer;
            int actualBytesRead;
            info.Resource.Read(buffer, info.Offset, FileSendInfo.BufferSize, out actualBytesRead);
            response.Body.Writer.WriteBytes(buffer, 0, actualBytesRead);

#if DEBUG
            if (Trace) Log.Debug("Sending chunk - {0}", info);
#endif

            //increase offset for next chunk
            info.Offset += actualBytesRead;

            //send chunk
            info.Context.Channel.Send(response, (sender, message, error) =>
                {
                    if (error == null)
                    {
                        //send successful -> queue for next chunk
                        QueueNextChunk(info);
                    }
                    else
                    {
                        if (NetSettings.EnableTracing && NetSettings.PublishError)
                        {
                            Log.Trace(this, LogType.Error, error, "Could not send chunk -> closing channel");
                            Log.Error(error);
                        }

                        //send failed -> quit
                        info.Context.Channel.Close();
                    }
                });
        }
        #endregion

        #region Queue Chunk
        void QueueNextChunk(FileSendInfo info)
        {
            //calculate the number of bytes to send next
            if (info.TotalLength - info.Offset > FileSendInfo.BufferSize) info.Length = FileSendInfo.BufferSize;
            else info.Length = info.TotalLength - info.Offset;

            if (info.Length > 0)
            {
                #region Has something to send

#if DEBUG
                if (Trace) Log.Debug("Queueing chunk - {0}", info);
#endif
                //queue
                TaskScheduler.Default.Run<FileSendInfo>((task) =>
                    {
                        SendChunk(task.Input);
                    }
                    , info)
                    //.Start.After(200)//throtling
                    .Done();
                //SendChunk(info);
                #endregion
            }
            else
            {                
                #region No more data -> quit

                #region Tracing
#if DEBUG
                if (Trace) Log.Debug("No more data to send -> quit");
                Log.Trace(this, LogType.Debug, null, "File send completed - {0}", info);
#endif
                #endregion

                //handle keep-alive
                if (!info.KeepAlive)
                {
                    info.Context.Channel.Close();

#if DEBUG
                    if (Trace) Log.Debug("Keep-alive was false in original request -> close channel");
#endif
                }
                #region Defensive tracing
                else
                {
#if DEBUG
                    if (Trace) Log.Debug("Keep-alive was true in original request");
#endif
                }
                #endregion

                //repool
                info.Repool();

                #endregion
            }
        }
        #endregion

        #region Process Headers
        /// <summary>
        /// This method is raised to process directives found in headers of the request.
        /// </summary>
        protected virtual void ProcessHeaders(FileSendInfo info)
        {
            IHttpHeaders headers = info.Context.Request.Headers;

            #region Handle If-Modified-Since header if available
            if (headers.ContainsKey(HttpRequestHeaderNames.If_Modified_Since))
            {
                string headerValue = headers[HttpRequestHeaderNames.If_Modified_Since];
                DateTime timestamp = DateTime.Parse(headerValue);
                if (info.Resource.LastModified <= timestamp)
                {
                    #region Resource has not been modified -> return status Not Modified

                    //HttpResponse response = new HttpResponse(HttpResponseStatus.NotModified);
                    //response.Body.Writer.WriteLine("Not modified");
                    //info.Context.Response = response;

                    ////suppress auto-sending so that my own response is sent
                    //info.AutoSend = false;

                    //return;
                    #endregion
                }
            }
            #endregion

            #region Process Range header

            if (headers.ContainsKey(HttpRequestHeaderNames.Range))
            {
                #region Range header found -> partial content

                try
                {
                    string rangeHeader = headers[HttpRequestHeaderNames.Range];
                    string[] parameters = rangeHeader.Split('=');
                    string[] ranges = parameters[1].Split('-');
                    long requestedOffset = long.Parse(ranges[0]);
                    long requestedEnd;
                    long requestedBytes;
                    if (ranges.Length == 2 && long.TryParse(ranges[1], out requestedEnd))
                    {
                        #region End range is specified
                        requestedBytes = (int)(requestedEnd - requestedOffset + 1);
                        #endregion
                    }
                    else
                    {
                        #region End range is not specified
                        requestedBytes = int.MaxValue;
                        #endregion
                    }

                    //apply to send info
                    info.Offset = requestedOffset;
                    info.Length = requestedBytes < info.TotalLength - requestedOffset ? requestedBytes : info.TotalLength - requestedOffset;
                }
                catch (Exception ex)
                {
                    if (NetSettings.EnableTracing && NetSettings.PublishError) Log.Trace(this, LogType.Error, ex, "Could not process Range header");
                }

                info.IsPartialContent = true;

                #endregion
            }
            else
            {
                #region Range header is not available -> Full content
                info.IsPartialContent = false;
                #endregion
            }

            #endregion
        }
        #endregion

        #region Determine Send Strategy
        /// <summary>
        /// Determine how the file should be sent (all at once or multiple chunks)
        /// </summary>
        protected virtual void DetermineSendStrategy(FileSendInfo info)
        {
            //calculate requested length
            long requestedLength = info.TotalLength - info.Offset;

            //determine whether file should be sent all at once or in chunks
            info.SendAllAtOnce = requestedLength <= FileSendInfo.BufferSize;
            if (requestedLength > FileSendInfo.BufferSize)
            {
                info.SendAllAtOnce = false;
#if DEBUG
                if (Trace) Log.Debug("Requested length ({0:#,#0}) is too big -> cut into chunks and send piece by piece", requestedLength);
#endif
            }
            else
            {
                info.SendAllAtOnce = true;
#if DEBUG
                if (Trace) Log.Debug("Requested length is small enough -> send all at once.");
#endif
            }
        }
        #endregion

        #region Send File
        /// <summary>
        /// Process sending a file to the client.
        /// </summary>
        protected virtual void SendFile(FileSendInfo info)
        {
            #region Tracing
#if DEBUG
            Log.Trace(this, LogType.Debug, null, "Begin to send file - {0}", info);
#endif
            #endregion

            HttpResponse response = new HttpResponse();

            //set mime type
            if (mMimeTypes.ContainsKey(info.FileExtension)) response.Headers[HttpResponseHeaderNames.Content_Type] = mMimeTypes[info.FileExtension];
            else response.Headers[HttpResponseHeaderNames.Content_Type] = mMimeTypes["default"];

            //set last-modified
            response.Headers[HttpResponseHeaderNames.Last_Modified] = info.Resource.LastModified.ToRfc822String();

            //set ETag
            response.Headers[HttpResponseHeaderNames.ETag] = info.Resource.ETag;

            //calculate requested length
            long requestedLength = info.Length < info.TotalLength - info.Offset ? info.Length : info.TotalLength - info.Offset;

            if (info.IsPartialContent)
            {
                #region Requested a partial content

                /*
                 * References:
                 * 
                 * http://dotnetslackers.com/articles/aspnet/Range-Specific-Requests-in-ASP-NET.aspx
                 * http://www.devx.com/dotnet/Article/22533
                 */

                response.Status = HttpResponseStatus.PartialContent;
                response.Headers[HttpResponseHeaderNames.Content_Range] = string.Format("bytes {0}-{1}/{2}", info.Offset, info.Offset + requestedLength - 1, info.TotalLength);

                #endregion
            }
            #region Defensive tracing
            else
            {
                //not partial request, move on
            }
            #endregion

            if (info.SendAllAtOnce)
            {
                #region Requesed length is small enough -> send all at once.

                //read from file
                byte[] buffer = info.Buffer;
                int actualBytesRead;
                info.Resource.Read(buffer, 0, (int)info.Length, out actualBytesRead);
                response.Body.Writer.WriteBytes(buffer, 0, actualBytesRead);

                //return response to client
                info.Context.Response = response;

                //repool info
                info.Repool();

                #region Tracing
#if DEBUG
                Log.Trace(this, LogType.Debug, null, "Send file completed (all at once) - {0}", info);
#endif
                #endregion

                #endregion

            }
            else
            {
                #region Requested length is too big -> cut into chunks and send piece by piece

                //let client know I support range
                response.Headers[HttpResponseHeaderNames.Accept_Ranges] = "bytes";

                //set content length
                response.Headers[HttpResponseHeaderNames.Content_Length] = requestedLength.ToString();

                //read the very first chunk
                info.Length = FileSendInfo.BufferSize;
                byte[] buffer = info.Buffer;
                int actualBytesRead;
                info.Resource.Read(buffer, info.Offset, FileSendInfo.BufferSize, out actualBytesRead);
                response.Body.Writer.WriteBytes(buffer, 0, actualBytesRead);
#if DEBUG
                if (Trace) Log.Debug("Sending response header + first chunk - {0}", info);
#endif

                info.Offset += actualBytesRead;

                //force keep alive
                info.Context.Request.Headers[HttpRequestHeaderNames.Connection] = HttpConnectionValues.KeepAlive;

                //make sure the server will not attempt to send the response automatically
                info.Context.AutoSend = false;
                info.Context.Response = response;

                //send the response header along with the first chunk
                info.Context.Channel.Send(response, (sender, message, error) =>
                {
                    if (error == null)
                    {
                        #region Send successfully

                        if (Trace) Log.Debug("First chunk sent successfully, sending the next chunk");

                        //queue the next chunk to be sent
                        QueueNextChunk(info);

                        #endregion
                    }
                    else
                    {
                        #region Failed to send -> close channel

                        if (NetSettings.EnableTracing && NetSettings.PublishError)
                        {
                            Log.Trace(this, LogType.Error, error, "Could not send response header + first chunk -> closing channel");
                            Log.Error(error);
                        }

                        //send failed -> quit
                        info.Context.Channel.Close();

                        #endregion
                    }
                });

                #endregion
            }
        }
        #endregion

        #region On Process
        /// <summary>
        /// Process an incoming request.
        /// </summary>
        protected virtual void OnProcess(IHttpRequestHandlingContext context, string localFilePath, string fileExtension)
        {
            IFileResource resource = mFileService.Get(localFilePath);

            if (resource != null)
            {
                #region File found

                //build send info
                FileSendInfo info = mPool.Allocate();
                info.Context = context;
                info.FileExtension = fileExtension;
                info.AbsoluteLocalFilePath = localFilePath;
                info.TotalLength = resource.Size;
                info.Offset = 0;
                info.Length = resource.Size;
                info.KeepAlive = context.Request.KeepAlive;
                info.Resource = resource;
#if DEBUG
                info.Uri = context.Request.Path;
#endif

                //process directives found in Headers section
                ProcessHeaders(info);

                //determine how to send
                if (info.AutoSend) DetermineSendStrategy(info);

                //send the file
                if (info.AutoSend) SendFile(info);

                #endregion
            }
            else
            {
                #region File not found -> skip
                #endregion
            }
        }
        #endregion

        #region IHttpRequestHandler Members

        #region Process
        void IHttpRequestHandler.Process(IHttpRequestHandlingContext context)
        {
            //get the filename to return
            string localFilePath = mPathTranslator.Translate(context.Request.Path);
            localFilePath = UrlEscape.Decode(localFilePath);
            string fileExtension = Path.GetExtension(localFilePath).ToLowerInvariant();

            if (!string.IsNullOrEmpty(fileExtension))
            {
                #region Has extension -> read the file and return it

                OnProcess(context, localFilePath, fileExtension);

                #endregion
            }
            else
            {
                #region No extension -> skip
                #endregion
            }
        }
        #endregion

        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor.
        /// </summary>
        public FileHandlerBase()
        {
            mPool = new ObjectPool<FileSendInfo>();
        }
        #endregion
    }
}
